//------------------------------------------------------------------------------
// <copyright file="XPathQilFactory.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <owner current="true" primary="true">Microsoft</owner>
//------------------------------------------------------------------------------

using System.Diagnostics;
using System.Xml.Schema;
using System.Xml.Xsl.Qil;
using System.Xml.Xsl.Runtime;

namespace System.Xml.Xsl.XPath {
    using Res   = System.Xml.Utils.Res;
    using T     = XmlQueryTypeFactory;

    internal class XPathQilFactory : QilPatternFactory {

        public XPathQilFactory(QilFactory f, bool debug) : base(f, debug) {
        }

        // Helper methods used in addition to QilPatternFactory's ones

        public QilNode Error(string res, QilNode args) {
            return Error(InvokeFormatMessage(String(res), args));
        }

        public QilNode Error(ISourceLineInfo lineInfo, string res, params string[] args) {
            return Error(String(XslLoadException.CreateMessage(lineInfo, res, args)));
        }

        public QilIterator FirstNode(QilNode n) {
            CheckNodeSet(n);
            QilIterator i = For(DocOrderDistinct(n));
            return For(Filter(i, Eq(PositionOf(i), Int32(1))));
        }

        public bool IsAnyType(QilNode n) {
            XmlQueryType xt = n.XmlType;
            bool result = !(xt.IsStrict || xt.IsNode);
            Debug.Assert(result == (xt.TypeCode == XmlTypeCode.Item || xt.TypeCode == XmlTypeCode.AnyAtomicType), "What else can it be?");
            return result;
        }

        [Conditional("DEBUG")]
        public void CheckAny(QilNode n) {
            Debug.Assert(n != null && IsAnyType(n), "Must be of 'any' type");
        }

        [Conditional("DEBUG")]
        public void CheckNode(QilNode n) {
            Debug.Assert(n != null && n.XmlType.IsSingleton && n.XmlType.IsNode, "Must be a singleton node");
        }

        [Conditional("DEBUG")]
        public void CheckNodeSet(QilNode n) {
            Debug.Assert(n != null && n.XmlType.IsNode, "Must be a node-set");
        }

        [Conditional("DEBUG")]
        public void CheckNodeNotRtf(QilNode n) {
            Debug.Assert(n != null && n.XmlType.IsSingleton && n.XmlType.IsNode && n.XmlType.IsNotRtf, "Must be a singleton node and not an Rtf");
        }

        [Conditional("DEBUG")]
        public void CheckString(QilNode n) {
            Debug.Assert(n != null && n.XmlType.IsSubtypeOf(T.StringX), "Must be a singleton string");
        }

        [Conditional("DEBUG")]
        public void CheckStringS(QilNode n) {
            Debug.Assert(n != null && n.XmlType.IsSubtypeOf(T.StringXS), "Must be a sequence of strings");
        }

        [Conditional("DEBUG")]
        public void CheckDouble(QilNode n) {
            Debug.Assert(n != null && n.XmlType.IsSubtypeOf(T.DoubleX), "Must be a singleton Double");
        }

        [Conditional("DEBUG")]
        public void CheckBool(QilNode n) {
            Debug.Assert(n != null && n.XmlType.IsSubtypeOf(T.BooleanX), "Must be a singleton Bool");
        }

        // Return true if inferred type of the given expression is never a subtype of T.NodeS
        public bool CannotBeNodeSet(QilNode n) {
            XmlQueryType xt = n.XmlType;
            // Do not report compile error if n is a VarPar, whose inferred type forbids nodes (SQLBUDT 339398)
            return xt.IsAtomicValue && !xt.IsEmpty && !(n is QilIterator);
        }

        public QilNode SafeDocOrderDistinct(QilNode n) {
            XmlQueryType xt = n.XmlType;

            if (xt.MaybeMany) {
                if (xt.IsNode && xt.IsNotRtf) {
                    // node-set
                    return DocOrderDistinct(n);
                } else if (!xt.IsAtomicValue) {
                    QilIterator i;
                    return Loop(i = Let(n),
                        Conditional(Gt(Length(i), Int32(1)),
                            DocOrderDistinct(TypeAssert(i, T.NodeNotRtfS)),
                            i
                        )
                    );
                }
            }

            return n;
        }

        public QilNode InvokeFormatMessage(QilNode res, QilNode args) {
            CheckString(res);
            CheckStringS(args);
            return XsltInvokeEarlyBound(QName("format-message"),
                XsltMethods.FormatMessage, T.StringX, new QilNode[] { res, args }
            );
        }

        #region Comparisons
        public QilNode InvokeEqualityOperator(QilNodeType op, QilNode left, QilNode right) {
            Debug.Assert(op == QilNodeType.Eq || op == QilNodeType.Ne);
            double opCode;
            left  = TypeAssert(left,  T.ItemS);
            right = TypeAssert(right, T.ItemS);

            switch (op) {
            case QilNodeType.Eq:    opCode = (double)XsltLibrary.ComparisonOperator.Eq; break;
            default:                opCode = (double)XsltLibrary.ComparisonOperator.Ne; break;
            }
            return XsltInvokeEarlyBound(QName("EqualityOperator"),
                XsltMethods.EqualityOperator, T.BooleanX, new QilNode[] { Double(opCode), left, right }
            );
        }

        public QilNode InvokeRelationalOperator(QilNodeType op, QilNode left, QilNode right) {
            Debug.Assert(op == QilNodeType.Lt || op == QilNodeType.Le || op == QilNodeType.Gt || op == QilNodeType.Ge);
            double opCode;
            left  = TypeAssert(left,  T.ItemS);
            right = TypeAssert(right, T.ItemS);

            switch (op) {
            case QilNodeType.Lt:    opCode = (double)XsltLibrary.ComparisonOperator.Lt; break;
            case QilNodeType.Le:    opCode = (double)XsltLibrary.ComparisonOperator.Le; break;
            case QilNodeType.Gt:    opCode = (double)XsltLibrary.ComparisonOperator.Gt; break;
            default:                opCode = (double)XsltLibrary.ComparisonOperator.Ge; break;
            }
            return XsltInvokeEarlyBound(QName("RelationalOperator"),
                XsltMethods.RelationalOperator, T.BooleanX, new QilNode[] { Double(opCode), left, right }
            );
        }
        #endregion

        #region Type Conversions
        [Conditional("DEBUG")]
        private void ExpectAny(QilNode n) {
            Debug.Assert(IsAnyType(n), "Unexpected expression type: " + n.XmlType.ToString());
        }

        public QilNode ConvertToType(XmlTypeCode requiredType, QilNode n) {
            switch (requiredType) {
            case XmlTypeCode.String     : return ConvertToString(n);
            case XmlTypeCode.Double     : return ConvertToNumber(n);
            case XmlTypeCode.Boolean    : return ConvertToBoolean(n);
            case XmlTypeCode.Node       : return EnsureNodeSet(n);
            case XmlTypeCode.Item       : return n;
            default                     : Debug.Fail("Unexpected XmlTypeCode: " + requiredType); return null;
            }
        }

        // XPath spec $4.2, string()
        public QilNode ConvertToString(QilNode n) {
            switch (n.XmlType.TypeCode) {
            case XmlTypeCode.Boolean :
                return (
                    n.NodeType == QilNodeType.True  ? (QilNode) String("true") :
                    n.NodeType == QilNodeType.False ? (QilNode) String("false") :
                    /*default: */                     (QilNode) Conditional(n, String("true"), String("false"))
                );
            case XmlTypeCode.Double :
                return (n.NodeType == QilNodeType.LiteralDouble
                    ? (QilNode) String(XPathConvert.DoubleToString((double)(QilLiteral)n))
                    : (QilNode) XsltConvert(n, T.StringX)
                );
            case XmlTypeCode.String :
                return n;
            default :
                if (n.XmlType.IsNode) {
                    return XPathNodeValue(SafeDocOrderDistinct(n));
                }

                ExpectAny(n);
                return XsltConvert(n, T.StringX);
            }
        }

        // XPath spec $4.3, boolean()
        public QilNode ConvertToBoolean(QilNode n) {
            switch (n.XmlType.TypeCode) {
            case XmlTypeCode.Boolean :
                return n;
            case XmlTypeCode.Double :
                // (x < 0 || 0 < x)  ==  (x != 0) && !Double.IsNaN(x)
                QilIterator i;
                return (n.NodeType == QilNodeType.LiteralDouble
                    ? Boolean((double)(QilLiteral)n < 0 || 0 < (double)(QilLiteral)n)
                    : Loop(i = Let(n), Or(Lt(i, Double(0)), Lt(Double(0), i)))
                );
            case XmlTypeCode.String :
                return (n.NodeType == QilNodeType.LiteralString
                    ? Boolean(((string)(QilLiteral)n).Length != 0)
                    : Ne(StrLength(n), Int32(0))
                );
            default:
                if (n.XmlType.IsNode) {
                    return Not(IsEmpty(n));
                }

                ExpectAny(n);
                return XsltConvert(n, T.BooleanX);
            }
        }

        // XPath spec $4.4, number()
        public QilNode ConvertToNumber(QilNode n) {
            switch (n.XmlType.TypeCode) {
            case XmlTypeCode.Boolean :
                return (
                    n.NodeType == QilNodeType.True  ? (QilNode) Double(1) :
                    n.NodeType == QilNodeType.False ? (QilNode) Double(0) :
                    /*default: */                 (QilNode) Conditional(n, Double(1), Double(0))
                );
            case XmlTypeCode.Double :
                return n;
            case XmlTypeCode.String :
                return XsltConvert(n, T.DoubleX);
            default:
                if (n.XmlType.IsNode) {
                    return XsltConvert(XPathNodeValue(SafeDocOrderDistinct(n)), T.DoubleX);
                }

                ExpectAny(n);
                return XsltConvert(n, T.DoubleX);
            }
        }

        public QilNode ConvertToNode(QilNode n) {
            if (n.XmlType.IsNode && n.XmlType.IsNotRtf && n.XmlType.IsSingleton) {
                return n;
            }
            return XsltConvert(n, T.NodeNotRtf);
        }

        public QilNode ConvertToNodeSet(QilNode n) {
            if (n.XmlType.IsNode && n.XmlType.IsNotRtf) {
                return n;
            }

            return XsltConvert(n, T.NodeNotRtfS);
        }

        // Returns null if the given expression is never a node-set
        public QilNode TryEnsureNodeSet(QilNode n) {
            if (n.XmlType.IsNode && n.XmlType.IsNotRtf) {
                return n;
            }
            if (CannotBeNodeSet(n)) {
                return null;
            }

            // Ensure it is not an Rtf at runtime
            return InvokeEnsureNodeSet(n);
        }

        // Throws an exception if the given expression is never a node-set
        public QilNode EnsureNodeSet(QilNode n) {
            QilNode result = TryEnsureNodeSet(n);
            if (result == null) {
                throw new XPathCompileException(Res.XPath_NodeSetExpected);
            }
            return result;
        }

        public QilNode InvokeEnsureNodeSet(QilNode n) {
            return XsltInvokeEarlyBound(QName("ensure-node-set"),
                XsltMethods.EnsureNodeSet, T.NodeSDod, new QilNode[] { n }
            );
        }
        #endregion

        #region Other XPath Functions
        public QilNode Id(QilNode context, QilNode id) {
            CheckNodeNotRtf(context);

            if (id.XmlType.IsSingleton) {
                return Deref(context, ConvertToString(id));
            }

            QilIterator i;
            return Loop(i = For(id), Deref(context, ConvertToString(i)));
        }

        public QilNode InvokeStartsWith(QilNode str1, QilNode str2) {
            CheckString(str1);
            CheckString(str2);
            return XsltInvokeEarlyBound(QName("starts-with"),
                XsltMethods.StartsWith, T.BooleanX, new QilNode[] { str1, str2 }
            );
        }

        public QilNode InvokeContains(QilNode str1, QilNode str2) {
            CheckString(str1);
            CheckString(str2);
            return XsltInvokeEarlyBound(QName("contains"),
                XsltMethods.Contains, T.BooleanX, new QilNode[] { str1, str2 }
            );
        }

        public QilNode InvokeSubstringBefore(QilNode str1, QilNode str2) {
            CheckString(str1);
            CheckString(str2);
            return XsltInvokeEarlyBound(QName("substring-before"),
                XsltMethods.SubstringBefore, T.StringX, new QilNode[] { str1, str2 }
            );
        }

        public QilNode InvokeSubstringAfter(QilNode str1, QilNode str2) {
            CheckString(str1);
            CheckString(str2);
            return XsltInvokeEarlyBound(QName("substring-after"),
                XsltMethods.SubstringAfter, T.StringX, new QilNode[] { str1, str2 }
            );
        }

        public QilNode InvokeSubstring(QilNode str, QilNode start) {
            CheckString(str);
            CheckDouble(start);
            return XsltInvokeEarlyBound(QName("substring"),
                XsltMethods.Substring2, T.StringX, new QilNode[] { str, start }
            );
        }

        public QilNode InvokeSubstring(QilNode str, QilNode start, QilNode length) {
            CheckString(str);
            CheckDouble(start);
            CheckDouble(length);
            return XsltInvokeEarlyBound(QName("substring"),
                XsltMethods.Substring3, T.StringX, new QilNode[] { str, start, length }
            );
        }

        public QilNode InvokeNormalizeSpace(QilNode str) {
            CheckString(str);
            return XsltInvokeEarlyBound(QName("normalize-space"),
                XsltMethods.NormalizeSpace, T.StringX, new QilNode[] { str }
            );
        }

        public QilNode InvokeTranslate(QilNode str1, QilNode str2, QilNode str3) {
            CheckString(str1);
            CheckString(str2);
            CheckString(str3);
            return XsltInvokeEarlyBound(QName("translate"),
                XsltMethods.Translate, T.StringX, new QilNode[] { str1, str2, str3 }
            );
        }

        public QilNode InvokeLang(QilNode lang, QilNode context) {
            CheckString(lang);
            CheckNodeNotRtf(context);
            return XsltInvokeEarlyBound(QName("lang"),
                XsltMethods.Lang, T.BooleanX, new QilNode[] { lang, context }
            );
        }

        public QilNode InvokeFloor(QilNode value) {
            CheckDouble(value);
            return XsltInvokeEarlyBound(QName("floor"),
                XsltMethods.Floor, T.DoubleX, new QilNode[] { value }
            );
        }

        public QilNode InvokeCeiling(QilNode value) {
            CheckDouble(value);
            return XsltInvokeEarlyBound(QName("ceiling"),
                XsltMethods.Ceiling, T.DoubleX, new QilNode[] { value }
            );
        }

        public QilNode InvokeRound(QilNode value) {
            CheckDouble(value);
            return XsltInvokeEarlyBound(QName("round"),
                XsltMethods.Round, T.DoubleX, new QilNode[] { value }
            );
        }
        #endregion
    }
}
